use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, path::PathBuf};

use super::{
    EnvironmentName, GitSha, LocalDepInfo, OnChainDepInfo, PackageName, PublishAddresses,
    RenderToml,
    toml_format::{expand_toml, flatten_toml},
};

/// An identifier for a node in the package graph, used to index into the
/// `[pinned.<environment>]` table
pub type PackageID = String;

/// The serialized lockfile format
// TODO: remove Debug and Clone
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ParsedLockfile {
    #[serde(rename = "move")]
    pub header: MoveHeader,
    #[serde(default)]
    pub pinned: BTreeMap<EnvironmentName, BTreeMap<PackageID, Pin>>,
}

pub type BuildConfig = BTreeMap<String, String>;

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct MoveHeader {
    pub version: Constant4,
}

#[derive(Serialize, Deserialize, Clone, Debug, Default)]
#[serde(into = "u8", try_from = "u8")]
pub struct Constant4;

/// A serialized entry in the `[pinned.<environment>.<package-id>]` table of the lockfile
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename = "kabob-case")]
pub struct Pin {
    /// Metadata about the package's source
    pub source: LockfileDependencyInfo,

    /// The stored `published-at` and `original-id` overrides (from the `published-at` and
    /// `original-id` fields in the manifest
    pub address_override: Option<PublishAddresses>,

    /// The environment that should be used for the dependency (as defined in the dependency's
    /// manifest but specified in the depender's manifest).
    ///
    /// E.g. if `a/Move.toml` contains
    /// ```toml
    /// environments.mainnet = "1234"
    /// dep-replacements.mainnet.b = { ..., use-environment="foo" }
    /// ```
    ///
    /// then use_environment for the `b` dependency would be `foo`
    pub use_environment: Option<EnvironmentName>,

    /// Contains the package's manifest digest. This is used to verify if a manifest has changed
    /// and re-pinning is required.
    pub manifest_digest: String,

    /// The package's dependencies, a map from the package name to the package id.
    pub deps: BTreeMap<PackageName, PackageID>,
}

/// A serialized pinned dependency in a lockfile
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(untagged)]
pub enum LockfileDependencyInfo {
    Local(LocalDepInfo),
    OnChain(OnChainDepInfo),
    Git(LockfileGitDepInfo),
    Root(RootDepInfo),
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RootDepInfo {
    pub root: bool,
}

/// A serialized lockfile dependency of the form `{git = "...", rev = "...", subdir = "..."}`
/// Note in particular that `rev` must exist and must be a `GitSha`
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct LockfileGitDepInfo {
    /// The repository containing the dependency
    #[serde(rename = "git")]
    pub repo: String,

    /// The path within the repository
    #[serde(rename = "subdir")]
    pub path: PathBuf,

    /// The git commit or branch for the dependency.
    pub rev: GitSha,
}

impl RenderToml for ParsedLockfile {
    /// Pretty-print `self` as a TOML document
    fn render_as_toml(&self) -> String {
        let mut toml = toml_edit::ser::to_document(self).expect("toml serialization succeeds");

        expand_toml(&mut toml);
        for (_, chain) in toml["pinned"].as_table_like_mut().unwrap().iter_mut() {
            for (_, item) in chain.as_table_like_mut().unwrap().iter_mut() {
                flatten_toml(item);
            }
        }

        toml.decor_mut()
            .set_prefix("# Generated by move; do not edit\n# This file should be checked in.\n\n");

        toml.to_string()
    }
}

impl RenderToml for LockfileDependencyInfo {
    fn render_as_toml(&self) -> String {
        let mut toml = toml_edit::ser::to_document(self).expect("toml serialization succeeds");

        flatten_toml(toml.as_item_mut());

        toml.to_string()
    }
}

impl TryFrom<u8> for Constant4 {
    type Error = &'static str;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        if value != 4 {
            Err("expected version 4")
        } else {
            Ok(Constant4)
        }
    }
}

impl From<Constant4> for u8 {
    fn from(_: Constant4) -> Self {
        4
    }
}

#[cfg(test)]
mod tests {
    use indoc::indoc;
    use test_log::test;
    use tracing::debug;

    use crate::schema::RenderToml;

    use super::ParsedLockfile;

    /// Parsing and then printing a well-formatted lockfile produces the original
    /// (this serves as an example output as well as a test)
    #[test]
    fn parse_then_render() {
        let lockfile = indoc!(
            r###"
            # Generated by move; do not edit
            # This file should be checked in.

            [move]
            version = 4

            [pinned.mainnet.Foo_0]
            source = { git = "...", subdir = "...", rev = "da39a3ee5e6b4b0d3255bfef95601890afd80709" }
            manifest_digest = "..."
            deps = { std = "MoveStdlib", sui = "Sui" }

            [pinned.mainnet.Foo_1]
            source = { git = "...", subdir = "...", rev = "da39a3ee5e6b4b0d3255bfef95601890afd80709" }
            manifest_digest = "..."
            deps = { std = "MoveStdlib", sui = "Sui" }

            [pinned.mainnet.MoveStdlib]
            source = { git = "...", subdir = "...", rev = "da39a3ee5e6b4b0d3255bfef95601890afd80709" }
            manifest_digest = "..."
            deps = {}

            [pinned.mainnet.Sui]
            source = { git = "...", subdir = "...", rev = "da39a3ee5e6b4b0d3255bfef95601890afd80709" }
            manifest_digest = "..."
            deps = { std = "MoveStdlib" }

            [pinned.testnet.MoveStdlib]
            source = { git = "...", subdir = "...", rev = "da39a3ee5e6b4b0d3255bfef95601890afd80709" }
            manifest_digest = "..."
            deps = {}

            [pinned.testnet.example]
            source = { local = "." }
            manifest_digest = "..."
            deps = { bar = "bar", foo = "Foo_0", non = "Foo_1", std = "MoveStdlib", sui = "Sui" }
            "###
        );

        let parsed: ParsedLockfile = toml_edit::de::from_str(lockfile).unwrap();
        let rendered = parsed.render_as_toml();
        debug!("Original lockfile:\n---\n{lockfile}\n---");
        debug!("Rendered lockfile:\n---\n{rendered}\n---");
        assert_eq!(rendered, lockfile);
    }
}
